/*******************************************************************************
 * Copyright (c) 2008, 2009 Composent, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Composent, Inc. - initial API and implementation
 ******************************************************************************/
package org.eclipse.ecf.internal.sync.resources.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.sync.IModelChange;
import org.eclipse.ecf.sync.IModelChangeMessage;
import org.eclipse.ecf.sync.resources.core.preferences.PreferenceConstants;
import org.osgi.framework.BundleContext;
import org.osgi.service.prefs.Preferences;

public class SyncResourcesCore extends Plugin implements
		IResourceChangeListener, IResourceDeltaVisitor {

	public static final String PLUGIN_ID = "org.eclipse.ecf.sync.resources.core"; //$NON-NLS-1$

	private static final int LIMIT = 25;

	private static final Hashtable channels = new Hashtable();

	private static LinkedList enqueuedChanges = new LinkedList();

	private static SyncResourcesCore instance;

	private Map resourceChanges = new HashMap();

	public static ResourcesShare getResourcesShare(ID containerID) {
		return (ResourcesShare) channels.get(containerID);
	}

	public static void addResourcesShare(ID containerID, ResourcesShare share) {
		channels.put(containerID, share);
	}

	public static ResourcesShare removeResourcesShare(ID containerID) {
		return (ResourcesShare) channels.remove(containerID);
	}

	public static Collection getResourceShares() {
		return Collections.unmodifiableCollection(channels.values());
	}

	/**
	 * Checks and returns whether the specified project is currently being
	 * shared.
	 */
	public static boolean isSharing(String projectName) {
		for (Iterator it = channels.values().iterator(); it.hasNext();) {
			ResourcesShare share = (ResourcesShare) it.next();
			if (share.isSharing(projectName)) {
				return true;
			}
		}
		return false;
	}

	private static boolean locked = false;

	/**
	 * Requests that all resource changes be ignored. That is, they should not
	 * be monitored for distribution to remote peers.
	 */
	public static void lock() {
		locked = true;
	}

	/**
	 * Unlocks by requesting that that all resource changes be monitored for
	 * distribution to remote peers.
	 */
	public static void unlock() {
		locked = false;
	}

	public boolean visit(IResourceDelta delta) throws CoreException {
		if (locked) {
			return false;
		}

		IResource resource = delta.getResource();
		int type = resource.getType();
		if (type == IResource.ROOT) {
			return true;
		}

		String projectName = resource.getProject().getName();
		boolean isSharing = isSharing(projectName);
		if (isSharing) {
			if (type == IResource.PROJECT) {
				return true;
			}
		} else {
			// this project is not being shared so we don't care about its
			// changes, return false
			return false;
		}

		// we are only interested in non-derived resources
		if (!resource.isDerived() && checkDelta(delta)) {
			for (Iterator it = channels.values().iterator(); it.hasNext();) {
				ResourcesShare share = (ResourcesShare) it.next();
				if (share.isSharing(projectName)) {
					IModelChange change = ResourceChangeMessage
							.createResourceChange(resource, delta.getKind());
					if (change != null) {
						List changes = (List) resourceChanges.get(share);
						if (changes == null) {
							changes = new ArrayList();
							resourceChanges.put(share, changes);
						}

						changes.add(change);
					}
				}
			}
		}
		return type == IResource.FOLDER;
	}

	private static boolean checkDelta(IResourceDelta delta) {
		return checkFlags(delta.getFlags()) || checkKind(delta.getKind());
	}

	private static boolean checkKind(int kind) {
		return kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED;
	}

	/**
	 * Checks the flags of a how a resource has been modified and returns
	 * whether this change should be propagated to remote peers.
	 * 
	 * @param flags
	 *            the detail flags of a resource delta
	 * @return <code>true</code> if the delta should be propagated,
	 *         <code>false</code> otherwise
	 */
	private static boolean checkFlags(int flags) {
		// we are only interested in content change, marker changes also fire
		// resource change events but we don't care about those, from
		// preliminary investigations, monitoring marker changes will cause
		// infinite loops
		return (flags & IResourceDelta.CONTENT) != 0;
	}

	public void resourceChanged(IResourceChangeEvent event) {
		try {
			event.getDelta().accept(this);
		} catch (CoreException e) {
			// ignored, this isn't possible as we never throw one
		} finally {
			try {
				// we're done, at least, "partially", distribute changes
				distributeChanges();
			} finally {
				// clear the cached changes
				resourceChanges.clear();
			}
		}
	}

	private void distributeChanges() {
		for (Iterator it = resourceChanges.entrySet().iterator(); it.hasNext();) {
			Entry entry = (Entry) it.next();
			ResourcesShare share = (ResourcesShare) entry.getKey();
			List changes = (List) entry.getValue();

			List messages = new ArrayList();
			for (int i = 0; i < changes.size(); i++) {
				ResourceChangeMessage change = (ResourceChangeMessage) changes
						.get(i);

				switch (change.getKind()) {
				case IResourceDelta.ADDED:
					if (getInt(PreferenceConstants.LOCAL_RESOURCE_ADDITION) == PreferenceConstants.IGNORE_VALUE) {
						change.setIgnored(true);
						continue;
					}
					break;
				case IResourceDelta.CHANGED:
					if (getInt(PreferenceConstants.LOCAL_RESOURCE_CHANGE) == PreferenceConstants.IGNORE_VALUE) {
						change.setIgnored(true);
						continue;
					}
					break;
				case IResourceDelta.REMOVED:
					if (getInt(PreferenceConstants.LOCAL_RESOURCE_DELETION) == PreferenceConstants.IGNORE_VALUE) {
						change.setIgnored(true);
						continue;
					}
					break;
				}

				IModelChangeMessage[] changeMessages = ResourcesSynchronizationStrategy
						.getInstance().registerLocalChange(change);
				messages.addAll(Arrays.asList(changeMessages));
			}

			try {
				if (!messages.isEmpty()) {
					IModelChangeMessage[] messagesArray = (IModelChangeMessage[]) messages
							.toArray(new IModelChangeMessage[messages.size()]);
					BatchModelChange batchChange = new BatchModelChange(
							messagesArray);
					share.send(Message.serialize(batchChange));

					add(batchChange);
				}
			} catch (ECFException e) {
				getDefault().getLog().log(
						new Status(IStatus.ERROR, PLUGIN_ID,
								"Could not send resource change message", e)); //$NON-NLS-1$
			}
		}
	}

	private static IView viewInstance;

	public static synchronized void setView(IView resourcesView) {
		viewInstance = resourcesView;
		if (resourcesView != null) {
			resourcesView.setInput(enqueuedChanges);
		}
	}

	public static synchronized void add(Object object) {
		if (enqueuedChanges.size() == LIMIT) {
			remove();
		}

		enqueuedChanges.addFirst(object);
		if (viewInstance != null) {
			viewInstance.add(object);
		}
	}

	private static synchronized void remove() {
		Object object = enqueuedChanges.removeLast();
		if (viewInstance != null) {
			viewInstance.remove(object);
		}
	}

	private static Preferences preferences;
	private static Preferences defaultPreferences;

	public SyncResourcesCore() {
		instance = this;
	}

	void attachListener() {
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
				IResourceChangeEvent.POST_CHANGE);
	}

	void detachListener() {
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext
	 * )
	 */
	public void start(BundleContext ctxt) throws Exception {
		super.start(ctxt);
		attachListener();
		preferences = new InstanceScope().getNode(SyncResourcesCore.PLUGIN_ID);
		defaultPreferences = new DefaultScope()
				.getNode(SyncResourcesCore.PLUGIN_ID);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
	 */
	public void stop(BundleContext context) throws Exception {
		instance = null;
		detachListener();
		super.stop(context);
	}

	public static SyncResourcesCore getDefault() {
		return instance;
	}

	public static int getInt(String key) {
		return preferences.getInt(key, defaultPreferences.getInt(key,
				PreferenceConstants.COMMIT_VALUE));
	}

}
